package com.ecar.oauth.provider.beans;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.util.DefaultJdbcListFactory;
import org.springframework.security.oauth2.common.util.JdbcListFactory;
import org.springframework.security.oauth2.provider.ClientAlreadyExistsException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationService;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

/**
 * Created by 30 on 2016/4/15.
 */
public class OAuthClientDetailsService implements ClientDetailsService, ClientRegistrationService {

	private static final Logger logger = LoggerFactory.getLogger(OAuthClientDetailsService.class);
	private static final String CLIENT_FIELDS_FOR_UPDATE =
		"resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " +
			"refresh_token_validity, additional_information";
	private static final String CLIENT_FIELDS = "client_secret, " + CLIENT_FIELDS_FOR_UPDATE;
	private static final String BASE_FIND_STATEMENT = "select client_id, " + CLIENT_FIELDS + " from oauth_client_details";
	private static final String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
	private static final String DEFAULT_SELECT_STATEMENT =
		"select client_id, " + CLIENT_FIELDS + ", trusted from oauth_client_details where client_id = ? and deleted = 0";
	private static final String DEFAULT_INSERT_STATEMENT =
		"insert into oauth_client_details (" + CLIENT_FIELDS + ", client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
	private static final String DEFAULT_UPDATE_STATEMENT = "update oauth_client_details set " +
		CLIENT_FIELDS_FOR_UPDATE.replaceAll(", ", "=?, ") + "=? where client_id = ?";
	private static final String DEFAULT_UPDATE_SECRET_STATEMENT =
		"update oauth_client_details set client_secret = ? where client_id = ?";
	private static final String DEFAULT_DELETE_STATEMENT = "delete from oauth_client_details where client_id = ?";

	private JsonMapper mapper = createJsonMapper();
	private RowMapper<ClientDetails> rowMapper = new ClientDetailsRowMapper();
	private String deleteClientDetailsSql;
	private String findClientDetailsSql;
	private String updateClientDetailsSql;
	private String updateClientSecretSql;
	private String insertClientDetailsSql;
	private String selectClientDetailsSql;
	private PasswordEncoder passwordEncoder;
	private JdbcListFactory listFactory;
	private final JdbcTemplate jdbcTemplate;

	public OAuthClientDetailsService(DataSource dataSource) {
		this.deleteClientDetailsSql = DEFAULT_DELETE_STATEMENT;
		this.findClientDetailsSql = DEFAULT_FIND_STATEMENT;
		this.updateClientDetailsSql = DEFAULT_UPDATE_STATEMENT;
		this.updateClientSecretSql = DEFAULT_UPDATE_SECRET_STATEMENT;
		this.insertClientDetailsSql = DEFAULT_INSERT_STATEMENT;
		this.selectClientDetailsSql = DEFAULT_SELECT_STATEMENT;
		this.passwordEncoder = NoOpPasswordEncoder.getInstance();
		Assert.notNull(dataSource, "DataSource required");
		this.jdbcTemplate = new JdbcTemplate(dataSource);
		this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(this.jdbcTemplate));
	}

	public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
		try {
			return jdbcTemplate.queryForObject(selectClientDetailsSql, new ClientDetailsRowMapper(), clientId);
		} catch (EmptyResultDataAccessException var4) {
			throw new NoSuchClientException("No client with requested id: " + clientId);
		}
	}

	public void addClientDetails(ClientDetails clientDetails) throws ClientAlreadyExistsException {
		try {
			jdbcTemplate.update(insertClientDetailsSql, getFields(clientDetails));
		} catch (DuplicateKeyException var3) {
			throw new ClientAlreadyExistsException("Client already exists: " + clientDetails.getClientId(), var3);
		}
	}

	public void updateClientDetails(ClientDetails clientDetails) throws NoSuchClientException {
		int count = jdbcTemplate.update(updateClientDetailsSql, getFieldsForUpdate(clientDetails));
		if (count != 1) {
			throw new NoSuchClientException("No client found with id = " + clientDetails.getClientId());
		}
	}

	public void updateClientSecret(String clientId, String secret) throws NoSuchClientException {
		int count = jdbcTemplate.update(updateClientSecretSql, passwordEncoder.encode(secret), clientId);
		if (count != 1) {
			throw new NoSuchClientException("No client found with id = " + clientId);
		}
	}

	public void removeClientDetails(String clientId) throws NoSuchClientException {
		int count = jdbcTemplate.update(deleteClientDetailsSql, clientId);
		if (count != 1) {
			throw new NoSuchClientException("No client found with id = " + clientId);
		}
	}

	public List<ClientDetails> listClientDetails() {
		return listFactory.getList(findClientDetailsSql, Collections.<String, Object>emptyMap(), rowMapper);
	}

	private Object[] getFields(ClientDetails clientDetails) {
		Object[] fieldsForUpdate = getFieldsForUpdate(clientDetails);
		Object[] fields = new Object[fieldsForUpdate.length + 1];
		System.arraycopy(fieldsForUpdate, 0, fields, 1, fieldsForUpdate.length);
		fields[0] = clientDetails.getClientSecret() != null ? passwordEncoder.encode(clientDetails.getClientSecret()) : null;
		return fields;
	}

	private Object[] getFieldsForUpdate(ClientDetails clientDetails) {
		String json = null;

		try {
			json = mapper.write(clientDetails.getAdditionalInformation());
		} catch (Exception var4) {
			logger.warn("Could not serialize additional information: " + clientDetails, var4);
		}

		return new Object[] {
			clientDetails.getResourceIds() != null ?
				StringUtils.collectionToCommaDelimitedString(clientDetails.getResourceIds()) : null,
			clientDetails.getScope() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails.getScope()) : null,
			clientDetails.getAuthorizedGrantTypes() != null ?
				StringUtils.collectionToCommaDelimitedString(clientDetails.getAuthorizedGrantTypes()) : null,
			clientDetails.getRegisteredRedirectUri() != null ?
				StringUtils.collectionToCommaDelimitedString(clientDetails.getRegisteredRedirectUri()) : null,
			clientDetails.getAuthorities() != null ?
				StringUtils.collectionToCommaDelimitedString(clientDetails.getAuthorities()) : null,
			clientDetails.getAccessTokenValiditySeconds(), clientDetails.getRefreshTokenValiditySeconds(),
			json, getAutoApproveScopes(clientDetails), clientDetails.getClientId()
		};
	}

	private String getAutoApproveScopes(ClientDetails clientDetails) {
		if (clientDetails.isAutoApprove("true")) return "true";

		HashSet scopes = new HashSet();
		for (String scope : clientDetails.getScope()) {
			if (clientDetails.isAutoApprove(scope)) scopes.add(scope);
		}
		return StringUtils.collectionToCommaDelimitedString(scopes);
	}

	public void setSelectClientDetailsSql(String selectClientDetailsSql) {
		this.selectClientDetailsSql = selectClientDetailsSql;
	}

	public void setDeleteClientDetailsSql(String deleteClientDetailsSql) {
		this.deleteClientDetailsSql = deleteClientDetailsSql;
	}

	public void setUpdateClientDetailsSql(String updateClientDetailsSql) {
		this.updateClientDetailsSql = updateClientDetailsSql;
	}

	public void setUpdateClientSecretSql(String updateClientSecretSql) {
		this.updateClientSecretSql = updateClientSecretSql;
	}

	public void setInsertClientDetailsSql(String insertClientDetailsSql) {
		this.insertClientDetailsSql = insertClientDetailsSql;
	}

	public void setFindClientDetailsSql(String findClientDetailsSql) {
		this.findClientDetailsSql = findClientDetailsSql;
	}

	public void setListFactory(JdbcListFactory listFactory) {
		this.listFactory = listFactory;
	}

	public void setRowMapper(RowMapper<ClientDetails> rowMapper) {
		this.rowMapper = rowMapper;
	}

	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		this.passwordEncoder = passwordEncoder;
	}

	private static JsonMapper createJsonMapper() {
		return ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", null) ?
			new JacksonMapper() : (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", null) ?
			new Jackson2Mapper() : new NotSupportedJsonMapper());
	}

	private static class NotSupportedJsonMapper implements JsonMapper {
		private NotSupportedJsonMapper() { }

		public String write(Object input) throws Exception {
			throw new UnsupportedOperationException("Neither Jackson 1 nor 2 is available so JSON conversion cannot be done");
		}

		public <T> T read(String input, Class<T> type) throws Exception {
			throw new UnsupportedOperationException("Neither Jackson 1 nor 2 is available so JSON conversion cannot be done");
		}
	}

	interface JsonMapper {
		String write(Object var1) throws Exception;

		<T> T read(String var1, Class<T> var2) throws Exception;
	}

	private static class JacksonMapper implements JsonMapper {
		private org.codehaus.jackson.map.ObjectMapper mapper;

		private JacksonMapper() {
			this.mapper = new org.codehaus.jackson.map.ObjectMapper();
		}

		public String write(Object input) throws Exception {
			return this.mapper.writeValueAsString(input);
		}

		public <T> T read(String input, Class<T> type) throws Exception {
			return this.mapper.readValue(input, type);
		}
	}

	private static class Jackson2Mapper implements JsonMapper {
		private ObjectMapper mapper;

		private Jackson2Mapper() {
			this.mapper = new ObjectMapper();
		}

		public String write(Object input) throws Exception {
			return this.mapper.writeValueAsString(input);
		}

		public <T> T read(String input, Class<T> type) throws Exception {
			return this.mapper.readValue(input, type);
		}
	}

	private static class ClientDetailsRowMapper implements RowMapper<ClientDetails> {
		private JsonMapper mapper;

		private ClientDetailsRowMapper() {
			this.mapper = createJsonMapper();
		}

		public ClientDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
			BaseClientDetails details = new BaseClientDetails(rs.getString(1), rs.getString(3),
															  rs.getString(4), rs.getString(5),
															  rs.getString(7), rs.getString(6));
			details.setClientSecret(rs.getString(2));
			if (rs.getObject(8) != null) details.setAccessTokenValiditySeconds(rs.getInt(8));
			if (rs.getObject(9) != null) details.setRefreshTokenValiditySeconds(rs.getInt(9));

			String json = rs.getString(10);
			if (json != null) {
				try {
					Map scopes = this.mapper.read(json, Map.class);
					details.setAdditionalInformation(scopes);
				} catch (Exception var6) {
					logger.warn("Could not decode JSON for additional information: " + details, var6);
				}
			}

			String scopes1 = rs.getString(11);
			if (scopes1 != null) details.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(scopes1));

			return details;
		}
	}
}
